package org.internna.expensetracker.model;

import java.util.Map;
import java.util.Set;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.HashMap;
import java.util.TreeSet;
import java.util.Currency;
import java.util.Calendar;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Collections;
import java.text.NumberFormat;
import javax.persistence.Query;
import javax.persistence.Column;
import javax.persistence.Temporal;
import javax.persistence.FetchType;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.NamedQuery;
import javax.persistence.CascadeType;
import javax.persistence.TemporalType;
import javax.persistence.NamedQueries;
import javax.validation.constraints.NotNull;
import org.hibernate.annotations.Type;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.roo.addon.entity.RooEntity;
import org.springframework.roo.addon.javabean.RooJavaBean;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.roo.addon.serializable.RooSerializable;
import org.internna.expensetracker.util.DateUtils;
import org.internna.expensetracker.util.StringUtils;
import org.internna.expensetracker.model.qif.Register;
import org.internna.expensetracker.model.support.Interval;
import org.internna.expensetracker.model.security.UserDetails;
import org.internna.expensetracker.model.support.InvestmentStatus;

import static org.springframework.util.StringUtils.hasText;

@RooEntity
@RooJavaBean
@RooSerializable
@NamedQueries({
	@NamedQuery(name = Account.FIND_ACCOUNT, query = "SELECT a FROM Account a WHERE owner = :owner AND name = :name"),
	@NamedQuery(name = Account.FIND_OPEN_ACCOUNTS, query = "SELECT a FROM Account a WHERE closed = false AND owner = :owner")
})
public class Account implements Comparable<Account> {

	private static final long serialVersionUID = -3804999694949321297L;

	public static final String FIND_ACCOUNT = "findAccount";
    public static final String FIND_OPEN_ACCOUNTS = "findOpenAccounts";

    private static final Log logger = LogFactory.getLog(Account.class);

    @NotNull
    private String name;

    private String isbn, accountNumber, description;

    private Boolean favorite;

    @NotNull
    @DateTimeFormat(style = "S-")
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModified;

    @NotNull
    private BigDecimal initialBalance;

    @NotNull
    private Boolean closed;

    @DateTimeFormat(style = "S-")
    @Temporal(TemporalType.TIMESTAMP)
    private Date opened;

    @NotNull
    @Column(updatable = false)
    @DateTimeFormat(style = "S-")
    @Temporal(TemporalType.TIMESTAMP)
    private Date created;

    @NotNull
    @ManyToOne
    private AccountType accountType;

    @OneToMany(cascade = CascadeType.REMOVE, fetch = FetchType.LAZY, mappedBy = "account")
    private Set<AccountTransaction> transactions = new TreeSet<AccountTransaction>();

    @NotNull
    @ManyToOne(fetch = FetchType.EAGER)
    private FinancialInstitution heldAt;

    @NotNull
    @ManyToOne(fetch = FetchType.EAGER)
    private UserDetails owner;

    @NotNull
    @Type(type = "org.hibernate.type.LocaleType")
    private Locale locale;

    /**
     * Adds a new transaction to this account.
     *
     * @param transaction any
     */
    public final void addTransaction(AccountTransaction transaction) {
        if (transaction != null) {
            if (transactions == null) {
                transactions = new TreeSet<AccountTransaction>();
            }
            if (transactions.add(transaction)) {
                transaction.setAccount(this);
            }
        }
    }

    /**
     * Calculates the current account balance. 
     *
     * @return a non null instance
     */
    public final BigDecimal calculateBalance() {
        return calculateBalance(new Date());
    }

    /**
     * Calculates the account balance at a given date.
     *
     * @return a non null instance
     */
    public final BigDecimal calculateBalance(Date to) {
        BigDecimal balance = BigDecimal.ZERO;
        Date end = to == null ? new Date() : to;
        if (!to.before(getOpened())) {
            if (initialBalance != null) {
                balance = initialBalance;
            }
            if (!CollectionUtils.isEmpty(transactions)) {
                for (AccountTransaction transaction : transactions) {
                    if (isBefore(transaction, end)) {
                        balance = balance.add(transaction.getAmount());
                    }
                }
            }
        }
        return balance;
    }

    /**
     * Checks if a transaction happened before a given date.
     *
     * @param transaction any
     * @param date any
     * @return true if the end date is null or the transaction date is before it
     */
    protected final boolean isBefore(AccountTransaction transaction, Date date) {
        return transaction == null || transaction.getOperationDate() == null ? false : date == null ? true : transaction.getOperationDate().before(date);
    }

    /**
     * Checks if the account balance is lower than ZERO.
     *
     * @return true if balance < 0
     */
    public final boolean isNegativeBalance() {
        return calculateBalance().compareTo(BigDecimal.ZERO) < 0;
    }

    public final boolean isNegativeEndBalance() {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.YEAR, 100);
        return calculateBalance(calendar.getTime()).compareTo(BigDecimal.ZERO) < 0;
    }

    public final List<AccountTransaction> getTransactionsInPeriod(Interval interval, Subcategory subcategory) {
        return getTransactionsInPeriod(interval, subcategory, false);
    }

    public final List<AccountTransaction> getTransactionsInPeriod(Interval interval, Subcategory subcategory, boolean filterTransfers) {
        return interval != null ? getTransactionsInPeriod(interval.getFrom(), interval.getTo(), subcategory, filterTransfers) : null;
    }

    public final List<AccountTransaction> getTransactionsInPeriod(Date from, Date to, Subcategory subcategory) {
        return getTransactionsInPeriod(from, to, subcategory, false);
    }

    @SuppressWarnings("unchecked")
    public final List<AccountTransaction> getTransactionsInPeriod(Date from, Date to, Subcategory subcategory, boolean filterTransfers) {
        Map<String, Object> params = new HashMap<String, Object>();
        StringBuilder select = new StringBuilder("select t from AccountTransaction t WHERE account = :account");
        if (from != null) {
        	params.put("from", from);
            select.append(" AND operationDate >= :from");
        }
        if (to != null) {
        	params.put("to", to);
            select.append(" AND operationDate <= :to");
        }
        if (subcategory != null) {
        	params.put("subcategory", subcategory);
        	select.append(" AND subcategory = :subcategory");
        }
        params.put("account", this);
        Query query = entityManager().createQuery(select.toString(), AccountTransaction.class);
        for (Map.Entry<String, Object> entry : params.entrySet()) {
            query.setParameter(entry.getKey(), entry.getValue());
        }
        return filterTransfers ? AccountTransaction.filterTransfers(query.getResultList()) : query.getResultList();
    }

    @SuppressWarnings("unchecked")
    public static List<Account> findOpenAccounts(UserDetails user) {
        List<Account> openAccounts = Collections.emptyList();
        if (user != null) {
            Query query = entityManager().createNamedQuery(Account.FIND_OPEN_ACCOUNTS, Account.class)
            	.setParameter("owner", user);
            openAccounts = query.getResultList();
        }
        return Collections.unmodifiableList(openAccounts);
    }

    @SuppressWarnings("unchecked")
    public static Account findAccount(String accountName, UserDetails user) {
    	Account account = null;
    	if ((user != null) && (hasText(accountName))) {
    		Query query = entityManager().createNamedQuery(Account.FIND_ACCOUNT, Account.class)
            	.setParameter("owner", user)
            	.setParameter("name", accountName);
    		try {
    			account = (Account) query.getSingleResult();
    		} catch (RuntimeException ex) {
    			logger.warn("Account [" + accountName + "] does not exist");
    		}
    	}
    	return account;
    }

    public Collection<InvestmentStatus> getCurrentInvestments() {
    	Map<Investment, InvestmentStatus> investments = new HashMap<Investment, InvestmentStatus>();
    	for (AccountTransaction transaction : transactions) {
    		if (transaction.getInvestment() != null) {
    			InvestmentTransaction investmentTransaction = transaction.getInvestment();
    			if (investmentTransaction != null) {
	    			Investment investment = investmentTransaction.getInvestment();
	    			InvestmentStatus status = investments.get(investment);
	    			if (status == null) {
	    				status = new InvestmentStatus(this, investment);
	    				investments.put(investment, status);
	    			}
	    			status.add(transaction);
    			}
    		}
    	}
    	return investments.values();
    }

    /**
     * Formats balance according to the account Locale.
     *
     * @return a non empty string
     */
    public final String getBalance() {
        return formatBalance(calculateBalance());
    }

    public final String getEndBalance() {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.YEAR, 100);
        return formatBalance(calculateBalance(calendar.getTime()));
    }

    private String formatBalance(BigDecimal balance) {
        NumberFormat formatter = NumberFormat.getCurrencyInstance(locale);
        return formatter.format(balance == null ? BigDecimal.ZERO : balance.doubleValue());
    }

    @Override public final int compareTo(Account account) {
        return account == null ? 1 : name.compareTo(account.getName()) == 0 ? getId().compareTo(account.getId()) : name.compareTo(account.getName());
    }

    public final String getCurrency() {
        Currency accountCurrency = Currency.getInstance(getLocale());
        return accountCurrency.getCurrencyCode();
    }

    public final boolean belongsTo(UserDetails user) {
        return user != null && owner != null && user.equals(owner);
    }

    public final boolean isCreditCard() {
        return Register.AccountType.CCARD.toString().equals(getAccountType().getTypeKey());
    }

    public final boolean isBankAccount() {
        return Register.AccountType.BANK.toString().equals(getAccountType().getTypeKey());
    }

    public final boolean isInvestmentAccount() {
        return Register.AccountType.INVST.toString().equals(getAccountType().getTypeKey());
    }

    public final String asQIF() {
    	StringBuilder builder = new StringBuilder("!Type:");
    	builder.append(isCreditCard() ? "CCard" : "Bank").append("\n");
    	builder.append(DateUtils.asQIF(opened)).append("\n");
    	builder.append("T").append(StringUtils.format(initialBalance)).append("\n");
    	builder.append("CX\nPOpening Balance\n");
    	builder.append("L[").append(name).append("]\n");
    	builder.append("^\n");
    	return builder.toString();
    }

    public Set<AccountTransaction> getTransactions() {
    	Set<AccountTransaction> ordered = new TreeSet<AccountTransaction>();
    	if (transactions != null) {
    		ordered.addAll(transactions);
    	}
		return ordered;
	}

	@Override public final String toString() {
        return getName();
    }

}
